package org.example;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Objects;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;

public class QTreeMapAggre {
    static final double lonBase = 100.0;
    static final double latBase = -6.366;
    static final double lanUpTo = 140.0;
    static final double latUpTo = 23.634;
    static final int lanGrids = 4000;
    static final int latGrids = 3000;
    static final int taX = lanGrids / 512 * 512;
    static final int taY = latGrids / 512 * 512;
    /**
     * 我们在测试区域范围(4000, 3000)内切分成了多张512x512的子地图，这是子地图的起点坐标。
     */
    static public class MapBase {
        int mx;
        int my;
        MapBase(int mx, int my) {
            this.mx = mx;
            this.my = my;
        }

        public MapBase() {
        }

        @Override
        public String toString() {
            return "MapBase{" +
                    "mx=" + mx +
                    ", my=" + my +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (o == null || getClass() != o.getClass()) return false;
            MapBase mapBase = (MapBase) o;
            return mx == mapBase.mx && my == mapBase.my;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mx, my);
        }

        public int getMx() {
            return mx;
        }

        public void setMx(int mx) {
            this.mx = mx;
        }

        public int getMy() {
            return my;
        }

        public void setMy(int my) {
            this.my = my;
        }
    }

    static public class Offset {
        int ox;
        int oy;
        Offset(int ox, int oy) {
            this.ox = ox;
            this.oy = oy;
        }

        public Offset() {

        }

        @Override
        public String toString() {
            return "Offset{" +
                    "ox=" + ox +
                    ", oy=" + oy +
                    '}';
        }
    }

    /**
     * 根据（lan,lat）找对应的地图编号,以及该点在地图内部的坐标偏移量。
     * coordinate
     */
    static public class MapCoordinate {
        double lan;
        double lat;
        MapCoordinate(double lan, double lat) {
            this.lan = lan;
            this.lat = lat;
        }
        MapBase getMapBase() {
            if (lan >= lanUpTo || lan < lonBase || lat >= latUpTo || lat <= latBase) {
                return null;
            }
            int offsetX = (int) ((lan - lonBase)*100);
            int offsetY = (int) ((lat - latBase)*100);
            int xId = (offsetX / 512) * 512;  // 0~511 => 0, 512 ~ 1023 => 512, ... etc
            int yId = (offsetY / 512) * 512;  //
            if (xId > taX) {
                xId = lanGrids - 512;
            }
            if (yId > taY) {
                yId = latGrids - 512;
            }
            return new MapBase(xId, yId);
        }

        Offset getMapOffset() {
            if (lan >= lanUpTo || lan < lonBase || lat >= latUpTo || lat <= latBase) {
                return null;
            }

            int gridX = (int) ((lan - lonBase)*100);
            int gridY = (int) ((lat - latBase)*100);
            int xInSubMap = gridX % 512;
            int yInSubMap = gridY % 512;

            int xId = (gridX / 512) * 512;  // 0~511 => 0, 512 ~ 1023 => 512, ... etc
            int yId = (gridY / 512) * 512;  //
            if (xId > taX) {
                xId = lanGrids - 512;
                xInSubMap = gridX - xId;
            }
            if (yId > taY) {
                yId = latGrids - 512;
                yInSubMap = gridX - yId;
            }
            return new Offset(xInSubMap, yInSubMap);
        }

        @Override
        public String toString() {
            return "MapCoordinate{" +
                    "lan=" + lan +
                    ", lat=" + lat +
                    '}';
        }
    }

    static class PathFindJobBase{
        public String toJson() {
            return "";
        }
    }
    /**
     * 一张子图，和它图内的几个点。
     */
    public static class MultiPointsInOneMap extends PathFindJobBase {
        MapBase mapBase;
        ArrayList<Offset> points;
        MultiPointsInOneMap() {
            this.points = new ArrayList<>();
            this.mapBase = new MapBase();
        }

        MultiPointsInOneMap(MapBase mapBase) {
            this.mapBase = mapBase;
            this.points = new ArrayList<>();
        }

        MultiPointsInOneMap(MapBase mapBase, ArrayList<Offset> points) {
            this.mapBase = mapBase;
            this.points = points;
        }

        void addOffset(Offset offset) {
            this.points.add(offset);
        }

        @Override
        public String toString() {
            return "MultiPointsInOneMap{\n" +
                    "mapBase=" + mapBase +
                    ", \npoints=" + points +
                    "\n}";
        }

        public String toJson() {
            JSONObject object = new JSONObject();
            object.put("type",0);

            JSONObject jsonMapBase = new JSONObject();
            jsonMapBase.put("mx", mapBase.mx);
            jsonMapBase.put("my", mapBase.my);
            object.put("mapBase", jsonMapBase);

            // 创建JSON数组
            JSONArray jsonArray = new JSONArray();
            for (var offset : points) {
                // 每个点作为一个JSON数组
                JSONArray pointArray = new JSONArray();
                pointArray.add(offset.ox);
                pointArray.add(offset.oy);
                jsonArray.add(pointArray);
            }
            object.put("points", jsonArray);
            return object.toJSONString();
        }

        public MapBase getMapBase() {
            return mapBase;
        }

        public void setMapBase(MapBase mapBase) {
            this.mapBase = mapBase;
        }

        public ArrayList<Offset> getPoints() {
            return points;
        }

        public void setPoints(ArrayList<Offset> points) {
            this.points = points;
        }
    }

    static class TwoPointsInTwoMap extends PathFindJobBase {
        MapBase mapBase1;
        Offset offset1;
        MapBase mapBase2;
        Offset offset2;
        public TwoPointsInTwoMap() {
            mapBase1 = new MapBase();
            mapBase2 = new MapBase();
            offset1 = new Offset();
            offset2 = new Offset();
        }

        public TwoPointsInTwoMap(MapBase mapBase1, Offset offset1, MapBase mapBase2, Offset offset2) {
            this.mapBase1 = mapBase1;
            this.offset1 = offset1;
            this.mapBase2 = mapBase2;
            this.offset2 = offset2;
        }

        @Override
        public String toString() {
            return "TwoPointsInTwoMap{" +
                    "mapBase1=" + mapBase1 +
                    ", offset1=" + offset1 +
                    ", mapBase2=" + mapBase2 +
                    ", offset2=" + offset2 +
                    '}';
        }

        public String toJson() {
            JSONObject o = new JSONObject();
            o.put("type", 1);

            JSONObject mapBase1 = new JSONObject();
            mapBase1.put("mx", this.mapBase1.mx);
            mapBase1.put("my", this.mapBase1.my);
            o.put("mapBase1", mapBase1);

            JSONObject mapBase2 = new JSONObject();
            mapBase2.put("mx", this.mapBase2.mx);
            mapBase2.put("my", this.mapBase2.my);
            o.put("mapBase2", mapBase2);

            JSONObject offset1 = new JSONObject();
            offset1.put("ox", this.offset1.ox);
            offset1.put("oy", this.offset1.oy);
            o.put("offset1", offset1);

            JSONObject offset2 = new JSONObject();
            offset2.put("ox", this.offset2.ox);
            offset2.put("oy", this.offset2.oy);
            o.put("offset2", offset2);

            return o.toJSONString();
        }

        public MapBase getMapBase1() {
            return mapBase1;
        }

        public void setMapBase1(MapBase mapBase1) {
            this.mapBase1 = mapBase1;
        }

        public Offset getOffset1() {
            return offset1;
        }

        public void setOffset1(Offset offset1) {
            this.offset1 = offset1;
        }

        public MapBase getMapBase2() {
            return mapBase2;
        }

        public void setMapBase2(MapBase mapBase2) {
            this.mapBase2 = mapBase2;
        }

        public Offset getOffset2() {
            return offset2;
        }

        public void setOffset2(Offset offset2) {
            this.offset2 = offset2;
        }
    }

    /**
     * python type points pair -> json type point pairs.
     */
    private static String getString() {
        String inputPoints = "[(121.35191085048595, 18.66431561231426), (121.30671673629185, 18.678697160768124), (121.193922127344, 18.62824002651597), (121.17514779693454, 18.634413239338823), (121.10810637554076, 18.652638794471763), (121.01402831740643, 18.626123884329893), (120.85686168606969, 18.646845605566536), (120.81072315243217, 18.654254927525486), (120.77055426167718, 18.627194972068807), (120.68913491795365, 18.587583536534382), (120.60771557423014, 18.54797210099996), (120.5630822237347, 18.508840434301614), (120.33634197190321, 18.431473420489027), (120.34230880535893, 18.378966036840943), (120.33952420903589, 18.340999361691054), (120.2849459385072, 18.22361848302763), (120.26271967785, 17.990816283582667), (120.22795643256258, 17.757857143323875), (120.32395642394123, 17.557953704516116), (120.36133764461127, 17.441895522986897), (120.2753383196744, 17.2491111163098), (120.2750687935569, 17.032358879030703), (120.31146276854444, 16.923457130252533), (120.31024461549156, 16.82287881351573), (120.30220748775554, 16.77844630078236), (120.28946427465016, 16.73357269686565), (120.27672106154476, 16.688699092948944), (120.26613368969967, 16.611908227959837), (120.27763048360082, 16.552384063430424), (120.28912727750196, 16.49285989890101), (120.32683650241731, 16.288874058155898), (120.26721214725859, 16.165784142216992), (120.29891766050167, 16.118272376829893)]";
        inputPoints = inputPoints.replace("(", "[").replace(")", "]");
        return inputPoints;
    }

    public static ArrayList<Point2D.Double> parseWithJackson(String jsonCoords) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        double[][] arrays = mapper.readValue(jsonCoords, double[][].class);

        ArrayList<Point2D.Double> points = new ArrayList<>();
        for (double[] pair : arrays) {
            points.add(new Point2D.Double(pair[0], pair[1]));
        }
        return points;
    }

    /**
     * Find all points in the same map and aggre to a job:
     * [point1, point2, ... point 2]
     * => [{map1: [point, point, ...]}, {map1:point, map2: point}, {map2:[point, point, ...], map3: [point, ...]}]
     * => [PathFindJobBase, PathFindJobBase, PathFindJobBase, ...]
     */
    static ArrayList<PathFindJobBase> from2dPointArray(ArrayList<Point2D.Double> pts) {
        ArrayList<MapCoordinate> arr = new ArrayList<>();
        pts.forEach(p -> {
            arr.add(new MapCoordinate(p.x, p.y));
        });

        ArrayList<PathFindJobBase> jobs = new ArrayList<>();

        MapCoordinate prev = null;

        for (var mapCoordinate : arr) {
            MapBase mapBase = mapCoordinate.getMapBase();
            if (mapBase == null) {
                System.out.println("map base of {" + mapCoordinate + "} is null");
                continue;
            }
            Offset offset = mapCoordinate.getMapOffset();
            if (prev != null) {   // skip 1st mapCoordinate
                if (jobs.isEmpty()) {  // jobs: add first element.
                    if (prev.getMapBase().equals(mapBase)) {   // using same map with previous point.
                        ArrayList<Offset> points = new ArrayList<>();
                        points.add(prev.getMapOffset());
                        points.add(offset);
                        MultiPointsInOneMap mp = new MultiPointsInOneMap(mapBase, points);
                        jobs.add(mp);
                    } else {
                        TwoPointsInTwoMap tp = new TwoPointsInTwoMap(prev.getMapBase(), prev.getMapOffset(), mapBase, offset);
                        jobs.add(tp);
                    }
                } else {
                    if (prev.getMapBase().equals(mapBase)) {
                        // same map with previous point.
                        PathFindJobBase j = jobs.get(jobs.size()-1);
                        if (j.getClass() == MultiPointsInOneMap.class) {
                            // and previous task is a 'same map task', just add points to the end.
                            ((MultiPointsInOneMap) j).points.add(offset);
                        } else {
                            // and previous task is 'two points cross two map', so create a new 'same map task'.
                            ArrayList<Offset> points = new ArrayList<>();
                            points.add(prev.getMapOffset());
                            points.add(offset);
                            MultiPointsInOneMap mp = new MultiPointsInOneMap(mapBase, points);
                            jobs.add(mp);
                        }
                    } else {
                        // diff map with previous point. just create a new 'two points cross two map' job.
                        TwoPointsInTwoMap tp = new TwoPointsInTwoMap(prev.getMapBase(), prev.getMapOffset(), mapBase, offset);
                        jobs.add(tp);
                    }
                }
            }
            prev = mapCoordinate;
        }

        return jobs;
    }

    static ArrayList<PathFindJobBase> getJobMapAndPoints() {
        String inputPoints = getString();
        //System.out.println(inputPoints);

        ArrayList<Point2D.Double> pts;
        try {
            pts = parseWithJackson(inputPoints);
            //pts.forEach(System.out::println);
        } catch (Exception e) {
            System.out.println(e);
            return new ArrayList<>();
        }
        return from2dPointArray(pts);
    }

    static PathFindJobBase jobFromJson(String jsonStr) {
        JSONObject j = JSONObject.parseObject(jsonStr);
        int type = j.getIntValue("type");
        if (type == 0) {
            // is MultiPointsInOneMap
            MultiPointsInOneMap n = new MultiPointsInOneMap();
            n.mapBase.mx = j.getJSONObject("mapBase").getIntValue("mx");
            n.mapBase.my = j.getJSONObject("mapBase").getIntValue("my");

            JSONArray jsonArray = j.getJSONArray("points");
            for (int i =0; i!=jsonArray.size(); i++) {
                JSONArray pointArray = jsonArray.getJSONArray(i);
                Offset o = new Offset(pointArray.getIntValue(0), pointArray.getIntValue(1));
                n.points.add(o);
            }
            return n;
        }
        else
        {
            var t = new TwoPointsInTwoMap();
            var mapBase1 = j.getJSONObject("mapBase1");
            t.mapBase1.mx = mapBase1.getIntValue("mx");
            t.mapBase1.my = mapBase1.getIntValue("my");

            var mapBase2 = j.getJSONObject("mapBase2");
            t.mapBase2.mx = mapBase2.getIntValue("mx");
            t.mapBase2.my = mapBase2.getIntValue("my");

            var offset1 = j.getJSONObject("offset1");
            t.offset1.ox = offset1.getIntValue("ox");
            t.offset1.oy = offset1.getIntValue("oy");

            var offset2 = j.getJSONObject("offset2");
            t.offset2.ox = offset2.getIntValue("ox");
            t.offset2.oy = offset2.getIntValue("oy");
            return t;
        }
    };

    public static void test2() {
        System.out.println("ordinary objects ...");
        var mergedMapPoints = getJobMapAndPoints();
        mergedMapPoints.forEach(System.out::println);

        System.out.println("objects to json string ...");
        ArrayList<String> s = new ArrayList<>();
        mergedMapPoints.forEach(a->{
            System.out.println(a.toJson());
            s.add(a.toJson());
        });

        System.out.println("parse objects from json ...");
        ArrayList<PathFindJobBase> r = new ArrayList<>();
        s.forEach(e->{
            r.add(jobFromJson(e));
        });
        r.forEach(System.out::println);

        // mergedMapPoints example with 2 elements:
        // - MapBase{mx=2048, my=2048},  <<-- in current version it means using map "2048_2048.npy"
        //   [Offset{ox=87, oy=455}, Offset{ox=82, oy=456}, Offset{ox=71, oy=451}, ... ]   <<-- points to find path.
        // - MapBase{mx=1536, my=2048},
        //   [Offset{ox=497, oy=431}, Offset{ox=498, oy=426}, Offset{ox=497, oy=422}, ... ]
        // In this example, has 3 job:
        // 1)find path for "array[0].Offset[0] -> array[0].Offset[1] -> array[0].Offset[1] ...", it works in one map.
        // 2)find path for "array[1].Offset[0] -> array[1].Offset[1] -> array[1].Offset[1] ...", it works in one map.
        // 3)find path for last element of array[0].offset and first element of array[1].offset
        //   that is "array[0].[-1] -> array[1].offset[0]". It works in two maps.
    }

    public static void main(String[] args) {
        //test2();
        test1();
    }

    public static void test1() {
        String message = "{\"id\":\"thisisauuid\", " +
                "\"coarse_path\": " +
                "[{\"lon\": 121.35191085048595, \"lat\": 18.66431561231426}, " +
                "{\"lon\": 121.30671673629185, \"lat\": 18.678697160768124}, " +
                "{\"lon\": 121.193922127344, \"lat\": 18.62824002651597}, " +
                "{\"lon\": 121.17514779693454, \"lat\": 18.634413239338823}, " +
                "{\"lon\": 121.10810637554076, \"lat\": 18.652638794471763}, " +
                "{\"lon\": 121.01402831740643, \"lat\": 18.626123884329893}, " +
                "{\"lon\": 120.85686168606969, \"lat\": 18.646845605566536}, " +
                "{\"lon\": 120.81072315243217, \"lat\": 18.654254927525486}, " +
                "{\"lon\": 120.77055426167718, \"lat\": 18.627194972068807}, " +
                "{\"lon\": 120.68913491795365, \"lat\": 18.587583536534382}, " +
                "{\"lon\": 120.60771557423014, \"lat\": 18.54797210099996}, " +
                "{\"lon\": 120.5630822237347, \"lat\": 18.508840434301614}, " +
                "{\"lon\": 120.33634197190321, \"lat\": 18.431473420489027}, " +
                "{\"lon\": 120.34230880535893, \"lat\": 18.378966036840943}, " +
                "{\"lon\": 120.33952420903589, \"lat\": 18.340999361691054}, " +
                "{\"lon\": 120.2849459385072, \"lat\": 18.22361848302763}, " +
                "{\"lon\": 120.26271967785, \"lat\": 17.990816283582667}, " +
                "{\"lon\": 120.22795643256258, \"lat\": 17.757857143323875}, " +
                "{\"lon\": 120.32395642394123, \"lat\": 17.557953704516116}, " +
                "{\"lon\": 120.36133764461127, \"lat\": 17.441895522986897}, "  +
                "{\"lon\": 120.2753383196744, \"lat\": 17.2491111163098}, "  +
                "{\"lon\": 120.2750687935569, \"lat\": 17.032358879030703}, "  +
                "{\"lon\": 120.31146276854444, \"lat\": 16.923457130252533}, "  +
                "{\"lon\": 120.31024461549156, \"lat\": 16.82287881351573}, "  +
                "{\"lon\": 120.30220748775554, \"lat\": 16.77844630078236}, "  +
                "{\"lon\": 120.28946427465016, \"lat\": 16.73357269686565}, "  +
                "{\"lon\": 120.27672106154476, \"lat\": 16.688699092948944}, "  +
                "{\"lon\": 120.26613368969967, \"lat\": 16.611908227959837}, "  +
                "{\"lon\": 120.27763048360082, \"lat\": 16.552384063430424}, "  +
                "{\"lon\": 120.28912727750196, \"lat\": 16.49285989890101}, "  +
                "{\"lon\": 120.32683650241731, \"lat\": 16.288874058155898}, "  +
                "{\"lon\": 120.26721214725859, \"lat\": 16.165784142216992}, "  +
                "{\"lon\": 120.29891766050167, \"lat\": 16.118272376829893}"  +
                "]}";
        var o = JSONObject.parseObject(message);
        var uuid = o.getString("id");
        var points = o.getJSONArray("coarse_path");
        ArrayList<Point2D.Double> pts = new ArrayList<>();
        for (int i=0; i!=points.size(); i++) {
            var lonLat = points.getJSONObject(i);
            var lon = lonLat.getDouble("lon");
            var lat = lonLat.getDouble("lat");
            pts.add(new Point2D.Double(lon, lat));
        }
        ArrayList<PathFindJobBase> jobs = from2dPointArray(pts);
        jobs.forEach(System.out::println);
    }
}

